How to Deploy a React + Flask Project

Posted by
on under

Welcome to the second part of my "React + Flask" series. In this episode we are going to deploy our project on a production server!

Have you missed the first part of this tutorial? You can find it here.

Namespacing API Routes

Before we begin looking at deployment options, I want to make a small change to the project as I left it in the first part of this series. As you recall, the Flask portion of the project was a little API with a single endpoint /time that returned the current Unix time.

When I started thinking about deployment I realized that it would be more convenient to have the API routes namespaced, so that they do not get mixed with any possible routes used by the React side. So I'm going to rename /time to /api/time. In the future, I'm going to implement any API routes with the same /api prefix, so that all the endpoints coming from the Python side are under the same root URL.

This change needs to be made in two places in the project. First, the api.py file needs to be updated:

@app.route('/api/time')
def get_current_time():
    return {'time': time.time()}

The second change is in React's src/App.js file, where the Flask endpoint is invoked:

function App() {
  const [currentTime, setCurrentTime] = useState(0);

  useEffect(() => {
    fetch('/api/time').then(res => res.json()).then(data => {
      setCurrentTime(data.time);
    });
  }, []);

  return (
    // ...
  }
}

Building the React Project

JavaScript projects that are intended for the browser have in general a build step that needs to be done before the application can be deployed to a production server. The purpose is to bundle all the source files and optimize their size so that they are served to clients as efficiently as possible.

For the React projects like this one, which were created with the create-react-app tool, a build can be triggered with the yarn build command:

$ yarn build
yarn run v1.21.1
$ react-scripts build
Creating an optimized production build...
Compiled successfully.

File sizes after gzip:

  40.06 KB  build/static/js/2.7f477b16.chunk.js
  777 B     build/static/js/runtime-main.228f8ff1.js
  717 B     build/static/js/main.bfcd0c95.chunk.js
  556 B     build/static/css/main.d1b05096.chunk.css

The project was built assuming it is hosted at the server root.
You can control this with the homepage field in your package.json.
For example, add this to build it for GitHub Pages:

  "homepage" : "http://myname.github.io/myapp",

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

  yarn global add serve
  serve -s build

Find out more about deployment here:

  bit.ly/CRA-deploy

   Done in 5.09s.

You can see that this is a fairly quick process. Once the build process completes the production-ready version of your application is left in the build directory:

build
├── asset-manifest.json
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── precache-manifest.ca735824199eb87023efb8cdfb52df29.js
├── robots.txt
├── service-worker.js
└── static
    ├── css
    │   ├── main.d1b05096.chunk.css
    │   └── main.d1b05096.chunk.css.map
    ├── js
    │   ├── 2.7f477b16.chunk.js
    │   ├── 2.7f477b16.chunk.js.LICENSE.txt
    │   ├── 2.7f477b16.chunk.js.map
    │   ├── main.bfcd0c95.chunk.js
    │   ├── main.bfcd0c95.chunk.js.map
    │   ├── runtime-main.228f8ff1.js
    │   └── runtime-main.228f8ff1.js.map
    └── media
        └── logo.5d5d9eef.svg

4 directories, 19 files

When deploying this application on a production server this build directory must be the web root, and the index.html file the main file from where the entire application is downloaded. Due to the large number of deployment platforms, it is impossible to cover every possible option. In this article I'm going to discuss two implementations.

Deploying on a Python Web Server

The first deployment method we are going to see is not a great solution in my opinion, but in many cases it is a very convenient one. We know we'll need to deploy the Flask API project, and we also know that Flask supports static files, so a logical implementation would be to set up the Flask project so that it serves the files that make up the React application in addition to the API endpoints.

To achieve this we need to point Flask's static folder to our build directory. This can be done with two options given when creating the Flask application instance:

  • static_folder tells Flask where is the static folder. By default this is a static directory located in the same directory where the application is. We can change it to point to build.
  • static_url_path tells Flask what is the URL prefix for all static files. By default this is /static. We can change it to the root URL, so that we do not need to prepend every static file with /static.

Here is how we can initialize the Flask application instance in the API project with these two arguments:

app = Flask(__name__, static_folder='../build', static_url_path='/')

The static_folder='../build' argument moves the static directory. Recall that the API project runs with the current directory set to the api, so to navigate to build we have to go up on level, and then down on build.

Without including the second argument, the React files would all be accessed with a /static URL prefix. So for example, you would use https://example.com/static/index.html to open the application. The static_url_path='/' argument removes the static file URL prefix, making the main URL https://example.com/index.html, which is more friendly.

You can rightly claim that having the explicitly give the index.html filename is unusual. Most production web servers provide a way to specify one or more default filenames for the server to return when the client asks for a URL that ends in a slash.

In Flask, we can emulate that functionality with a route:

@app.route('/')
def index():
    return app.send_static_file('index.html')

With this route, when the client requests the https://example.com/ the server will send the contents of the index.html static file.

The Flask project now is able to serve both the React and Flask sides, so now all that is left is to deploy it like you would for a Flask only project. For example, you can use gunicorn or uWSGI.

I assume that if you are using this method you are either creating a quick test, or else you are deploying to a managed platform such as Heroku. If instead you use this method on a server that you manage and you intend to run your application robustly, you should make sure the Python web server executes as a service, and restarts in the event of a crash or the server being power cycled. I show in the next section how to do that using systemd.

Deploying on Nginx

If you intend to deploy your project to a production server that you fully control, then there is a much better option. If you followed my Flask Mega-Tutorial, you may remember the Linux deployment chapter. In this chapter I present a complete deployment that includes a Gunicorn server for the Python project, and nginx as a reverse proxy in front of it.

Initial Setup

Before you begin with the deployment tasks you should install your project on your server, and follow all the steps required to set up the project for development, as shown in the first part of this series. These steps include:

  • Installing Node.js, yarn and Python
  • Installing all the JavaScript dependencies for the project
  • Creating a Python virtual environment and installing Python dependencies

Serving the React Application

Installing nginx is fairly simple, as it comes as a package in most Linux distributions. On Ubuntu, you can install it with apt-get:

$ sudo apt-get install nginx

After the installation completes, you can connect to your server with your web browser and you should see the nginx welcome page:

Nginx welcome page

The sites that nginx serves are configured in the /etc/nginx/sites-available and /etc/nginx/sites-enabled directories. The "available" directory is a work area where you can define your sites, while the "enabled" directory has symlinks to files that are in the other directory that are currently active. This two-directory system allows you to have several configuration scripts defined, but not all of them enabled at the same time.

The default configuration that is created when you install nginx in Ubuntu includes one site, called default. A file with this name is created in the sites-available directory, and a symlink to it is added in sites-enabled. This is the configuration that creates the welcome page. We can start by deleting the symlink to remove this site:

$ sudo rm /etc/nginx/sites-enabled/default

Now let's create a configuration for the React project. Assuming you have your React application in the /home/ubuntu/react-flask-app directory, the following configuration for nginx serves its build directory:

server {
    listen 80;
    root /home/ubuntu/react-flask-app/build;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
}

This script has an important simplification. You can see that it is written to listen on port 80, which is the HTTP port used for content that is not encrypted. In today's world this is almost never a good idea, but I do not want to complicate this tutorial with the largely unrelated topic of implementing HTTP encryption. When you are ready to look at encryption you can check my other tutorial specific to that topic.

The root directive tells nginx the location of the root directory, from where all the static files will be served. This is pointing to the build subdirectory of our project.

The index directive is used to configure the default file that is returned when the client requests a URL that ends with a slash or doesn't have a path at all. The main file of our React project is index.html so that is what we need to use.

The location blocks are a core part of the nginx configuration, in which rules can be defined for different groups of URLs. In this first version of the configuration the only location is /, meaning it applies to all URLs. The try_files directive defined for this location indicates that nginx needs to serve the path given in the request URL as a file ($uri) for a first choice. If a file does not exist with the requested name, then it should try to serve it as a directory ($uri/). If neither of the two options work then it should return a 404 error, which is the "resource not found" error defined in the HTTP protocol.

You can write the above configuration script to /etc/nginx/sites-available/react-flask-app.nginx. Then enable it by creating a symlink to it:

$ sudo ln -s /etc/nginx/sites-available/react-flask-app.nginx /etc/nginx/sites-enabled/react-flask-app.nginx

Finally, tell nginx to reload its configuration to incorporate these changes:

$ sudo systemctl reload nginx

Now if you connect to your server from a web browser you will have the React application running. Since the Flask part of the project isn't running yet, the API call to obtain the current time will not work at this point.

React application without API

Serving the Flask Application

The React side is now working, so let's look at the API. During development the Flask development web server was used, but this isn't a server that is robust enough for production use. What we want is a production-ready web server. For this tutorial I'm going to use Gunicorn, which needs to be installed in the virtual environment of our API project:

(venv) $ pip install gunicorn

The command to run our API project with Gunicorn is as follows:

$ gunicorn -b 127.0.0.1:5000 api:app

This tells Gunicorn to start the project on port 5000 of the internal network (127.0.0.1). We do not want to run this web server on the public network interface, since all accesses from the outside world are going to come through nginx. The api:app nomenclature is a common notation for WSGI applications, where the first part indicates the module or package where the application is defined, and the second part is the name of the variable in that module that holds it.

Since this application is going to run on a production server, we have to make sure it is always running, so starting the application from the command-line is not really sufficient. For example, if the server is power cycled we want it to start automatically when the server is back up, in the same way nginx does.

Unix has several process monitoring systems that are dedicated to the purpose of keeping services running. In most modern Linux distributions, systemd is used for this purpose. If you want to learn the details of how to set up a Flask application under systemd, see this other article. The solution consists in adding a service file in the /etc/systemd/system directory, with all the information that systemd needs to run the Flask application. Below you can see the service file I created for the Flask API project in this article:

[Unit]
Description=A simple Flask API
After=network.target

[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/react-flask-app/api
ExecStart=/home/ubuntu/react-flask-app/api/venv/bin/gunicorn -b 127.0.0.1:5000 api:app
Restart=always

[Install]
WantedBy=multi-user.target

Copy the above configuration file to /etc/systemd/system/flask-react-app.service (you will need to use sudo), and then tell systemd to reload:

$ sudo systemctl daemon-reload

Assuming there were no errors during the reload, you can now tell systemd to start the Flask application as a service:

$ sudo systemctl start react-flask-app

If you want to verify that the service is now running, you can use the status command:

$ sudo systemctl status react-flask-app
● react-flask-app.service - <a description of your application>
   Loaded: loaded (/etc/systemd/system/react-flask-app.service; disabled; vendor preset: enabled)
   Active: active (running) since Sat 2020-03-28 16:54:09 UTC; 17h ago
 Main PID: 10449 (gunicorn)
    Tasks: 2 (limit: 1152)
   CGroup: /system.slice/react-flask-app.service
           ├─10449 /home/ubuntu/react-flask-app/api/venv/bin/python3 /home/ubuntu/react-flask-app/api/venv/bin/gunicorn -b 127.0.0.1:5000 api:app
           └─10493 /home/ubuntu/react-flask-app/api/venv/bin/python3 /home/ubuntu/react-flask-app/api/venv/bin/gunicorn -b 127.0.0.1:5000 api:app

Mar 28 16:54:09 demobox systemd[1]: Started <a description of your application>.
Mar 28 16:54:10 demobox gunicorn[10449]: [2020-03-28 16:54:10 +0000] [10449] [INFO] Starting gunicorn 20.0.4
Mar 28 16:54:10 demobox gunicorn[10449]: [2020-03-28 16:54:10 +0000] [10449] [INFO] Listening at: http://127.0.0.1:5000 (10449)
Mar 28 16:54:10 demobox gunicorn[10449]: [2020-03-28 16:54:10 +0000] [10449] [INFO] Using worker: sync
Mar 28 16:54:10 demobox gunicorn[10449]: [2020-03-28 16:54:10 +0000] [10493] [INFO] Booting worker with pid: 10493

The main part of this output is the active (running) status, which indicates that the service started. At this point systemd is actively monitoring this process, and any time it finds the process isn't running it will start a new copy, including in the case of the server being rebooted.

The part that remains is to tell nginx to act as a reverse-proxy for the API service. This can be done with a second location block that is specific to all URLs that begin with /api:

server {
    listen 80;
    root /home/ubuntu/react-flask-app/build;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

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

The proxy_params included file has common definitions that are used when nginx acts as a reverse proxy. The proxy_pass command gives the address where the proxied service is listening for requests.

Now you can reload nginx one more time:

$ sudo systemctl reload nginx

And now when you refresh the page on your web browser you should see that the API call to get the time is working:

React + Flask application

Caching Configuration

A detail that is often ignored or glossed over is how to properly configure caching. The files created by React's build tend to be somewhat large, so it is a good idea to have proper caching directives set in place so that clients do not have to download the application every time they connect to the server.

The create-react-app project has a documentation section on caching, in which they suggest to give a caching time of one year to all the builds in build/static, and to disable caching for all other files, so let's implement this in the nginx configuration:

server {
    listen 80;
    root /home/ubuntu/react-flask-app/build;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
        add_header Cache-Control "no-cache";
    }

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

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

The first change is the addition of add_header Cache-Control "no-cache" in the main location block. This effectively turns caching off for all the static files in the React project.

To provide different caching instructions for the files in build/static (which are those with URLs starting with /static), a new location block for /static is added. In this block the expiration is set to one year, and the cache is set to public, which indicates that any cache is free to store a copy of this file for the indicated period of time.

Reload nginx one last time:

$ sudo systemctl reload nginx

And now the project is fully configured!

Remaining Work

To complete a production deployment on a Linux server there are a few more tasks that you should consider that are out of scope for this article but are extremely important.

I hinted at this earlier, your server will more than likely need to be configured with encryption and an SSL certificate to meet the security standards your users will expect. A fairly common nginx set up is to configure your site on port 443 with encryption, and then configure a secondary server on port 80 that just redirects all requests coming on http:// to the same URL on https://.

You should also consider adding a firewall to your server such as the Uncomplicated Firewall or ufw. This will help you control which ports are open to external connections on your server. For the example server in this article I would just open ports 22 (SSH), 80 (HTTP) and 443 (HTTPS). Any other ports should be closed to the outside, even if you are running processes on the server that listen on those ports.

While these two are the most important, there are a number of smaller tasks related to the security of your server that you should also take care of. The Linux deployment chapter of my Flask Mega-Tutorial is a good guide to follow on securing your server.

Conclusion

I hope this was a helpful tutorial and now you are on your way to deploying your React + Flask project.

Do you have any other questions regarding this project? Let me know below in the comments!

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

77 comments
  • #26 Miguel Grinberg said

    @Rakesh: if you can deploy a regular Flask project with IIS, then use my first method to deploy the combined Flask + React project.

  • #27 Antal said

    Hola Miguel! First of all thank you very much for the great explanations and tutorials.
    I was wondering would it be possible to namespace another route for another site that could be run simultaneously so I could have multiple sites running on the same droplet?
    Like defining
    proxy_pass
    http://localhost:5001; instead of 5000 and run some application server on that port for example with the namespace of /application2?
    Then using
    @app.route('/application2')
    def index2():
    return something

    Actually what I am thinking about is installing a WordPress for a CMS back-end on /application2, maybe this is not so sensible but I am wondering if it is possible.

  • #28 Hero said

    Thanks for the useful tutorial. I have a react-flask app followed instructions, I have a big issue and it's hard to mention my problem, just like that:
    I have a ubuntu server and I deployed my project. I had a route /search in Nginx and react-route and route /searchapi in API. It's true when I run the project from my home to /search, However when I refreshed page which has a route named /search in UI, then request URL is just /search instead of /searchapi as I wanted . => It showed 404 NOT FOUND
    Hoping for your help.

  • #29 Miguel Grinberg said

    @Antal: Sure, this is done in the nginx configuration, you can have as many sites as you like, each on a different port and under a different domain or root URL.

  • #30 Miguel Grinberg said

    @Hero: if your application uses client-side routes, you have to make sure that all those client-side routes are answered in the same way as my / route in this example, by returning the index.html page.

  • #31 Hero said

    @Miguel Grinberg
    I think my mistake is Nginx config with https (certbot).
    Please, access my IP and help me.
    https://trithucyhoc.ddns.net/
    input "Tim" => Enter
    Refresh this page => Error when it just consisted json instead of UI frontend.

    <hr />

    =>This is my Nginx config
    listen 443 ssl;

        root /home/MedicalKnowledge/build;
    
        index index.html index.htm index.nginx-debian.html;
    
        server_name trithucyhoc.ddns.net www.trithucyhoc.ddns.net;
    
        location / {
                root /home/MedicalKnowledge/build;
                index index.html index.htm;
                try_files $uri $uri/ /index.html;
                add_header Cache-Control "no-cache";
        }
        location ~* /search {
                include proxy_params;
                proxy_pass http://localhost:5000;
                #try_files $uri $uri/ /index.html;
        }
    

    => Hoping for your help. Thanks u.

  • #32 Miguel Grinberg said

    @Hero: you changed the URL in your page to /search. The nginx config for this URL sends you first to your API, then to React. Why did you change the URL in your React app? Leave it as / and the refresh will work. Or else use different route URLs for front and back end.

  • #33 Antal said

    Hello Miguel! Thanks for your earlier answer.
    Just one question, I couldn't find the answer to it in the Linux deployment article either:

    How do you set up Nginx so it "knows" that for example websita.com should have a location block with one root and websiteb.com would have a location block with a different root? Would be cool to set it up to be able to serve different domain names and back-ends from the same Droplet sounds like a very useful skill to have.

    Thank you again!

  • #34 Hero said

    Sorry, I really didn't understand your mind. I didn't change URL in my react app. And my react router also has path is '/search'
    Or your mind is in Nginx config, /search {} => / {}, then where I can put proxy_pass is ? Could you give me more details?

  • #35 Miguel Grinberg said

    @Hero: you can't use the same URL for the front and back end. Use different URLs, then make sure that nginx serves the frontend when all the frontend URLs are requested.

  • #36 Miguel Grinberg said

    @Antal: you just add multiple server definitions on the sites-enabled directory. As long as you set the server name, nginx will be able to determine which configuration to use based on the domain that comes with the URL.

  • #37 Hero said

    @ Miguel, I changed into different URLs. I confused to use location /search and router search in react-route. If I only use location / to direct all, then where I can put proxy_pass to change proxy to API?
    Thanks so much, Miguel

  • #38 Miguel Grinberg said

    @Hero: I show how to do this in this article. Have you followed my directions? You have to start all your API routes with a prefix such as /api, then use that prefix for the API location block.

  • #39 Dave said

    Hi there.
    Nice tutorial, but I have a question.

    What if I have routes in my React application for example localhost: 3000 / user / 1 but in my Flask backend and don't have any route similar to that, and when i access to localhost: 5000 / user / 1 obviously a 404 error . My backend has already a route to serve that / user / 1.

    The problem is, both apps have routes, but in the end who serve the routes is the flask app. What can I do in this case?

  • #40 Miguel Grinberg said

    @Dave: if your React app understood those routes they would never make it to the server. There is a problem in your React app that is missing routes. Review your router components, the problem is likely there.

  • #41 Evan C said

    Miguel, thanks so much for this, it was extremely helpful. I'm now going through your flask mega-tutorial as follow up. As I'm going through it, though, I was wondering if you had any thoughts on best practices for forms (with CSRF protection) and authentication when using this Flask/React architecture.

  • #42 Miguel Grinberg said

    @Evan: If you use any form of authentication for your Flask API that is not based on a cookie, then the token gives you CSRF protection.

  • #43 sakugawa said

    Hi Miguel, thanks for such a helpful tutorial.
    Is it possible to run a create-react-app app with react-router in the Flask server without proxies like nginx? With nginx, I configure the root to the build directory and set location like "location / {try_files $uri /index.html}" and when I change the router in browser it works well the same as running nodejs server. Can I do the same thing without proxy?
    I don't know if I explain clearly, thanks.

  • #44 Miguel Grinberg said

    @sakugawa: Did you try doing this and had problems? It should work. The only thing when you use the router is that you need to improve how the server handles client-side routes in the same way it handles the top-level URL, which is to serve the React application. This is something that you need to do with any web server, so it applies to nginx and to any Python web server.

  • #45 Evan said

    Thanks again for posting this. I just finished combining these two articles with your the Flask-Mega Tutorial using React for the front end, and am happy to report that everything is working. I only ran into one issue, and it was tied to React Router. With the set up above, if you try to navigate to any page other than home using the address bar, you get redirected to the 404 page. I simply changed "try_files $uri $uri/ =404;" to "try_files $uri /index.html =404" in the nginx sites-available file and it seems to work. I also have a catch all route in my React application to redirect to home if the path doesn't exist. Just wanted to share in case anyone else was planning to use React Router and was running into this problem.

  • #46 envsion said

    Hi Miguel,
    This problem has been bothering me for a long time.
    1. sudo systemctl status react-flask-app
    2.● react-flask-app.service -
    3. Loaded: loaded (/etc/systemd/system/react-flask-app.service; disabled; vendor preset: enabled)

    why not line 3 Loaded disabled, but application work normal. How to fixed disabled to enable.

  • #47 Miguel Grinberg said

    @envsion: Have you enabled your server? Use systemctl enable react-flask-app.

  • #48 Luke Hartman said

    Excellent tutorial. It's worth noting that Nginx by default does not include /etc/nginx/sites-enabled. This is something that Ubuntu and maybe Debian add. This can be easily added following https://stackoverflow.com/a/17415606

  • #49 Eduardo Vicentini said

    Hello.

    I would like to know what is the name of the file created on the step after deleting the default file from nginx and where this file is saved. The configuration file with this inside:

    server {
    listen 80;
    root /home/ubuntu/react-flask-app/build;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }
    

    }

  • #50 Miguel Grinberg said

    @Eduardo: the name of this file does not matter. You can call it "microblog", or "microblog.nginx".

Leave a Comment